package world;

import java.util.ArrayList;
import java.util.HashMap;

import media.AudioManager;
import media.Sounds;
import objects.AbstractObject;
import objects.Crystal;
import objects.ItemBlock;
import objects.Player;
import objects.Tower;
import screens.LevelCompleteWindow;
import screens.PauseMenuWindow;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.assets.loaders.TileMapRendererLoader.TileMapParameter;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.tiled.TileAtlas;
import com.badlogic.gdx.graphics.g2d.tiled.TileMapRenderer;
import com.badlogic.gdx.graphics.g2d.tiled.TiledLayer;
import com.badlogic.gdx.graphics.g2d.tiled.TiledLoader;
import com.badlogic.gdx.graphics.g2d.tiled.TiledMap;
import com.badlogic.gdx.graphics.g2d.tiled.TiledObject;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.EdgeShape;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.me.jnrgame.JnR;

public class WorldManager {

	private OrthographicCamera camera;
	private TiledMap currentWorld;
	private TileAtlas tileAtlas;
	private FileHandle fileHandle;
	private TileMapRenderer worldRenderer;
	private int[] numberOfLayers = { 0 };
	private int totalWidth;
	private int totalHeight;
	public float PIXELS_PER_METER = 96f;
	private World world;
	private SpriteBatch hudBatch;
	private TextButton leftButton;
	private TextButton rightButton;
	private TextButton jumpButton;
	Player player;
	Skin skin;
	private SpriteBatch spriteBatch;
	private PauseMenuWindow pauseMenu;
	private JnR game;
	private float screenWidth;
	private float screenHeight;
	private ArrayList<AbstractObject> gameObjects = new ArrayList<AbstractObject>();
	private ArrayList<Body> toRemove = new ArrayList<Body>();
	private float levelTime;
	private StopWatch stopWatch;
	private Label timeLabel;

	private Box2DDebugRenderer debugRenderer;

	public WorldManager(OrthographicCamera _camera, float _screenWidth,
			float _screenHeight, JnR _game) {
		game = _game;
		spriteBatch = new SpriteBatch();
		screenWidth = _screenWidth;
		screenHeight = _screenHeight;
		// TODO Auto-generated constructor stub
		camera = _camera;
		stopWatch = new StopWatch();
		camera.position.set(_screenWidth / 2, _screenHeight / 2, 0);
		createHUD();

		/**
		 * You can set the world's gravity in its constructor. Here, the gravity
		 * is negative in the y direction (as in, pulling things down).
		 */

		/**
		 * Boxes are defined by their "half width" and "half height", hence the
		 * 2 multiplier.
		 */

		/**
		 * The character should not ever spin around on impact.
		 */

		/**
		 * The density and friction of the jumper were found experimentally.
		 * Play with the numbers and watch how the character moves faster or
		 * slower.
		 */

		System.nanoTime();

	}

	public void resize(int width, int height) {

	}

	public void renderWorld() {
		// System.out.println("PPM: " + PIXELS_PER_METER + " scaling factor: "
		// + scalingFactor + " scaled block size: " + PIXELS_PER_METER);
		if (crystalCount == 0 && !game.isGamePaused()) {

			game.pauseGame(true);
			AudioManager.pauseMusic();

			stage.addActor(new LevelCompleteWindow("WON", skin, game, stage,
					stopWatch.getElapsedTime()));
		}
		if (player.isGameOver() && !game.isGamePaused()) {

			game.pauseGame(true);
			stopWatch.stop();
			AudioManager.pauseMusic();
			AudioManager.playSound(Sounds.Death);
			pauseMenu = new PauseMenuWindow("Game Over", skin, game,
					game.getLevelScreen(), true);
			game.setPauseMenu(pauseMenu);
			stage.addActor(pauseMenu);
		}
		if (!game.isGamePaused()) {
			Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
			Gdx.gl20.glBlendEquation(GL20.GL_BLEND);
			worldRenderer.getProjectionMatrix().set(camera.combined);
			Vector3 tmp = new Vector3();
			tmp.set(0, 0, 0);

			camera.unproject(tmp);
			world.step(Gdx.graphics.getDeltaTime(), 3, 3);
			removeObjects();
			movePlayer();
			renderBackground();
			worldRenderer.render((int) tmp.x, (int) tmp.y, screenWidth,
					screenHeight);

			renderObjects(tmp);
			renderPlayer();
			renderHUD();

			// renderDebugFrame();
			camera.update();
			checkPhoneKeys();

		}

	}

	public void renderBackground() {
		spriteBatch.begin();
		// spriteBatch.draw(texture,camera.position.x,camera.position.y);

		spriteBatch.draw(texture, camera.position.x - (screenWidth / 2 + 20),
				camera.position.y - screenHeight / 2, (int) (camera.position.x
						- screenWidth / 2 + 20),
				(int) (camera.position.y - screenHeight / 2),
				(int) (screenWidth + 20), 480);
		spriteBatch.end();
	}

	Texture texture;

	public void loadWorld(int worldNumber) {
		System.gc();

		fileHandle = Gdx.files.internal("data/world/packs");
		currentWorld = TiledLoader.createMap(Gdx.files
				.internal("data/world/lvl" + worldNumber + "/level.tmx"));
		texture = new Texture(Gdx.files.internal("data/back.png"));

		hudBatch = new SpriteBatch();
		world = new World(new Vector2(0.0f, -10.0f), true);

		tileAtlas = new TileAtlas(currentWorld, fileHandle);
		worldRenderer = new TileMapRenderer(currentWorld, tileAtlas, 8, 8);

		totalWidth = currentWorld.width * currentWorld.tileWidth;
		totalHeight = currentWorld.height * currentWorld.tileHeight;
		levelTime = 0f;

		player = new Player(world, camera);
		world.setContactListener(new WorldContactListener(this, player));
		loadCollisions("data/world/packs/tiles boundaries");

		loadObjects(currentWorld.objectGroups.get(0).objects);
		debugRenderer = new Box2DDebugRenderer();

		stopWatch.start();
		System.runFinalization();
		System.gc();
	}

	int crystalCount;
	Sprite sprite;

	public void loadObjects(ArrayList<TiledObject> tiledObjects) {
		crystalCount = 0;
		for (int i = 0; i < tiledObjects.size(); i++) {
			if (tiledObjects.get(i).type.toString().equals(BodyID.COIN)) {
				gameObjects.add(new Crystal(currentWorld, world, tiledObjects
						.get(i).x, tiledObjects.get(i).y));
				crystalCount++;
			} else {
				if (tiledObjects.get(i).type.toString().equals(
						BodyID.TOWER_LEFT)) {
					gameObjects.add(new Tower(currentWorld, world, tiledObjects
							.get(i).x, tiledObjects.get(i).y, false));

				} else {
					if (tiledObjects.get(i).type.toString().equals(
							BodyID.TOWER_RIGHT)) {
						gameObjects.add(new Tower(currentWorld, world,
								tiledObjects.get(i).x, tiledObjects.get(i).y,
								true));
					}
				}
			}
		}

	}

	Sprite coinSprite = new Sprite(new Texture(
			Gdx.files.internal("data/sprites/crystal.png")));

	public void renderObjects(Vector3 tmp) {
		spriteBatch.setProjectionMatrix(camera.combined);
		spriteBatch.begin();
		for (int i = 0; i < gameObjects.size(); i++) {
			if (!gameObjects.get(i).getBody().isActive()) {
				world.destroyBody(gameObjects.get(i).getBody());

				gameObjects.remove(i);
			} else {
				if (gameObjects.get(i).position.x * PIXELS_PER_METER > tmp.x - 64
						&& gameObjects.get(i).position.x * PIXELS_PER_METER < tmp.x
								+ 64 + screenWidth) {
					if (gameObjects.get(i).getBody().getUserData() == BodyID.COIN)
						gameObjects.get(i).setSprite(coinSprite);
					gameObjects.get(i).renderObject(camera, spriteBatch);
				}else{if(gameObjects.get(i).getBody().getUserData() == BodyID.COIN)
					gameObjects.get(i).removeSprite();}
			}
		}

		spriteBatch.end();
	}

	public void readToRemove(Body body) {
		toRemove.add(body);
		if (body.getUserData() == BodyID.COIN) {
			crystalCount--;

		}

	}

	private void removeObjects() {

		for (int i = 0; i < toRemove.size(); i++) {

			toRemove.get(i).setActive(false);
		}
		toRemove.clear();
	}

	public TiledMap getWorld() {
		return currentWorld;
	}

	private Stage stage;
	Label crystalsLabel;
	Label timerLabel;

	public void createHUD() {
		stage = new Stage(screenWidth, screenHeight, true);

		TextureAtlas menuAtlas = new TextureAtlas(
				Gdx.files.internal("data/screen-layouts/level/uiskin.atlas"));
		Gdx.input.setInputProcessor(stage);
		skin = new Skin(
				Gdx.files.internal("data/screen-layouts/level/uiskin.json"),
				menuAtlas);

		leftButton = new TextButton("L", skin);
		leftButton.setSize(80f, 80f);
		leftButton.setPosition(15, 15);
		stage.addActor(leftButton);
		// leftButton.addListener(new ChangeListener() {
		// @Override
		// public void changed(ChangeEvent event, Actor actor) {
		// // TODO Auto-generated method stub
		//
		// }
		//
		// });

		rightButton = new TextButton("R", skin);
		rightButton.setSize(80f, 80f);
		rightButton.setPosition(leftButton.getWidth() + 30, 15);
		stage.addActor(rightButton);
		// rightButton.addListener(new InputListener() {
		// public void click(Actor actor, float x, float y) {
		// // game.setScreen( game.getOptionsScreen() );
		// System.out.println("options");
		// }
		// });
		jumpButton = new TextButton("^", skin);
		jumpButton.setSize(80f, 80f);
		jumpButton.setPosition(screenWidth - (jumpButton.getWidth() + 15), 15);
		stage.addActor(jumpButton);

		crystalsLabel = new Label("Crystals: " + crystalCount, skin);
		crystalsLabel.setSize(150f, 40f);
		crystalsLabel.setPosition(15, screenHeight - 45f);
		stage.addActor(crystalsLabel);

		timeLabel = new Label("Time: " + stopWatch.getElapsedTime(), skin);
		timeLabel.setSize(150f, 40f);
		timeLabel.setPosition(700, screenHeight - 45f);
		stage.addActor(timeLabel);

	}

	public void addHUD() {

		if (stage.getActors().size == 0) {
			stage.addActor(leftButton);
			stage.addActor(rightButton);
			stage.addActor(jumpButton);
			stage.addActor(crystalsLabel);
			stage.addActor(timeLabel);
		}
	}

	public TileMapRenderer getWorldRenderer() {
		return worldRenderer;
	}

	public void checkPhoneKeys() {

		if (Gdx.input.isKeyPressed(Keys.MENU)
				|| Gdx.input.isKeyPressed(Keys.ESCAPE)) {
			System.out.println("menu key pressed");
			game.pauseGame(true);
			pauseMenu = new PauseMenuWindow("Pause", skin, game,
					game.getLevelScreen(), false);
			game.setPauseMenu(pauseMenu);
			stage.addActor(pauseMenu);

		}
	}

	public void renderPlayer() {
		player.renderPlayer(spriteBatch);

	}

	public void pauseStopWatch() {
		stopWatch.pause();
	}

	public void resumeStopWatch() {
		stopWatch.resume();
	}

	public void renderPauseMenu() {

		stage.act(Math.min(Gdx.graphics.getDeltaTime(), 1 / 30f));
		stage.draw();
	}

	public void renderHUD() {

		hudBatch.begin();
		crystalsLabel.setText("Crystals: " + crystalCount);
		timeLabel.setText("Time: " + stopWatch.getElapsedTime());
		stage.act(Gdx.graphics.getDeltaTime());
		stage.draw();
		hudBatch.end();

	}

	public void renderDebugFrame() {

		debugRenderer.render(world, camera.combined.scale(PIXELS_PER_METER,
				PIXELS_PER_METER, PIXELS_PER_METER));

	}

	Vector2 hero_normal = new Vector2(0, 0);

	public void movePlayer() {

		boolean moveLeft = false;
		boolean moveRight = false;
		boolean doJump = false;

		System.nanoTime();
		if (rightButton.isPressed()) {
			moveRight = true;
		} else {
			if (Gdx.input.isKeyPressed(Keys.D)) {
				moveRight = true;
			}
		}

		if (leftButton.isPressed()) {
			moveLeft = true;
		} else {
			if (Gdx.input.isKeyPressed(Keys.A)) {
				moveLeft = true;
			}
		}

		if (jumpButton.isPressed()) {
			doJump = true;
		} else {
			if (Gdx.input.isKeyPressed(Keys.W)) {
				doJump = true;
			}
		}

		player.movePlayer(moveLeft, moveRight, doJump);

		checkCameraBoundaries();

	}

	public void checkCameraBoundaries() {

		if (camera.position.x < screenWidth / 2) {
			camera.position.x = screenWidth / 2;
		}
		if (camera.position.x >= totalWidth - screenWidth / 2) {
			camera.position.x = totalWidth - screenWidth / 2;
		}

		if (camera.position.y < screenHeight / 2) {
			camera.position.y = screenHeight / 2;
		}
		if (camera.position.y >= totalHeight - screenHeight / 2) {

			camera.position.y = totalHeight - screenHeight / 2;
		}

		camera.update();

	}

	public Player getPlayer() {
		return player;
	}

	public void loadCollisions(String collisionsFile) {
		/**
		 * Detect the tiles and dynamically create a representation of the map
		 * layout, for collision detection. Each tile has its own collision
		 * rules stored in an associated file.
		 * 
		 * The file contains lines in this format (one line per type of tile):
		 * tileNumber XxY,XxY XxY,XxY
		 * 
		 * Ex:
		 * 
		 * 3 0x0,31x0 ... 4 0x0,29x0 29x0,29x31
		 * 
		 * For a 32x32 tileset, the above describes one line segment for tile #3
		 * and two for tile #4. Tile #3 has a line segment across the top. Tile
		 * #1 has a line segment across most of the top and a line segment from
		 * the top to the bottom, 30 pixels in.
		 */

		FileHandle fh = Gdx.files.internal(collisionsFile);
		String collisionFile = fh.readString();
		String lines[] = collisionFile.split("\\r?\\n");

		HashMap<Integer, ArrayList<LineSegment>> tileCollisionJoints = new HashMap<Integer, ArrayList<LineSegment>>();

		/**
		 * Some locations on the map (perhaps most locations) are "undefined",
		 * empty space, and will have the tile type 0. This code adds an empty
		 * list of line segments for this "default" tile.
		 */
		tileCollisionJoints.put(Integer.valueOf(0),
				new ArrayList<LineSegment>());

		for (int n = 0; n < lines.length; n++) {
			String cols[] = lines[n].split(" ");
			int tileNo = Integer.parseInt(cols[0]);

			ArrayList<LineSegment> tmp = new ArrayList<LineSegment>();

			for (int m = 1; m < cols.length; m++) {
				String coords[] = cols[m].split(",");

				String start[] = coords[0].split("x");
				String end[] = coords[1].split("x");

				tmp.add(new LineSegment(Integer.parseInt(start[0]), Integer
						.parseInt(start[1]), Integer.parseInt(end[0]), Integer
						.parseInt(end[1])));
			}

			tileCollisionJoints.put(Integer.valueOf(tileNo), tmp);
		}

		ArrayList<LineSegment> collisionLineSegments = new ArrayList<LineSegment>();

		for (int y = 0; y < currentWorld.height; y++) {
			for (int x = 0; x < currentWorld.width; x++) {
				int tileType = currentWorld.layers.get(0).tiles[(currentWorld.height - 1)
						- y][x];
				if (tileType < tileCollisionJoints.size()) {
					for (int n = 0; n < tileCollisionJoints.get(
							Integer.valueOf(tileType)).size(); n++) {
						LineSegment lineSeg = tileCollisionJoints.get(
								Integer.valueOf(tileType)).get(n);

						addOrExtendCollisionLineSegment(x
								* currentWorld.tileWidth + lineSeg.start().x, y
								* currentWorld.tileHeight - lineSeg.start().y
								+ 32,
								x * currentWorld.tileWidth + lineSeg.end().x,
								y * currentWorld.tileHeight - lineSeg.end().y
										+ 32, collisionLineSegments);
					}

				}
			}
		}

		BodyDef groundBodyDef = new BodyDef();
		groundBodyDef.type = BodyDef.BodyType.StaticBody;
		Body groundBody = world.createBody(groundBodyDef);
		groundBody.setUserData(BodyID.GROUND);
		for (LineSegment lineSegment : collisionLineSegments) {
			EdgeShape environmentShape = new EdgeShape();

			environmentShape.set(lineSegment.start().mul(1 / PIXELS_PER_METER),
					lineSegment.end().mul(1 / PIXELS_PER_METER));
			groundBody.createFixture(environmentShape, 0);
			environmentShape.dispose();
		}

		/**
		 * Drawing a boundary around the entire map. We can't use a box because
		 * then the world objects would be inside and the physics engine would
		 * try to push them out.
		 */

		EdgeShape mapBounds = new EdgeShape();
		// mapBounds.set(new Vector2(0.0f, 0.0f), new Vector2(totalWidth
		// / pixelsPerMeter, 0.0f));
		// groundBody.createFixture(mapBounds, 0);

		mapBounds.set(new Vector2(0.0f, totalHeight / PIXELS_PER_METER),
				new Vector2(totalWidth / PIXELS_PER_METER, totalHeight
						/ PIXELS_PER_METER));
		groundBody.createFixture(mapBounds, 0);

		mapBounds.set(new Vector2(0.0f, 0.0f), new Vector2(0.0f, totalHeight
				/ PIXELS_PER_METER));
		groundBody.createFixture(mapBounds, 0);

		mapBounds.set(new Vector2(totalWidth / PIXELS_PER_METER, 0.0f),
				new Vector2(totalWidth / PIXELS_PER_METER, totalHeight
						/ PIXELS_PER_METER));
		groundBody.createFixture(mapBounds, 0);

		mapBounds.dispose();
	}

	private void addOrExtendCollisionLineSegment(float lsx1, float lsy1,
			float lsx2, float lsy2, ArrayList<LineSegment> collisionLineSegments) {
		LineSegment line = new LineSegment(lsx1, lsy1, lsx2, lsy2);

		boolean didextend = false;

		for (LineSegment test : collisionLineSegments) {
			if (test.extendIfPossible(line)) {
				didextend = true;
				break;
			}
		}

		if (!didextend) {
			collisionLineSegments.add(line);
		}
	}

	public Stage getStage() {

		return stage;

	}

	private class LineSegment {
		private Vector2 start = new Vector2();
		private Vector2 end = new Vector2();

		/**
		 * Construct a new LineSegment with the specified coordinates.
		 * 
		 * @param x1
		 * @param y1
		 * @param x2
		 * @param y2
		 */
		public LineSegment(float x1, float y1, float x2, float y2) {
			start = new Vector2(x1, y1);
			end = new Vector2(x2, y2);
		}

		/**
		 * The "start" of the line. Start and end are misnomers, this is just
		 * one end of the line.
		 * 
		 * @return Vector2
		 */
		public Vector2 start() {
			return start;
		}

		/**
		 * The "end" of the line. Start and end are misnomers, this is just one
		 * end of the line.
		 * 
		 * @return Vector2
		 */
		public Vector2 end() {
			return end;
		}

		/**
		 * Determine if the requested line could be tacked on to the end of this
		 * line with no kinks or gaps. If it can, the current LineSegment will
		 * be extended by the length of the passed LineSegment.
		 * 
		 * @param lineSegment
		 * @return boolean true if line was extended, false if not.
		 */
		public boolean extendIfPossible(LineSegment lineSegment) {
			/**
			 * First, let's see if the slopes of the two segments are the same.
			 */
			double slope1 = Math.atan2(end.y - start.y, end.x - start.x);
			double slope2 = Math.atan2(lineSegment.end.y - lineSegment.start.y,
					lineSegment.end.x - lineSegment.start.x);

			if (Math.abs(slope1 - slope2) > 1e-9) {
				return false;
			}

			/**
			 * Second, check if either end of this line segment is adjacent to
			 * the requested line segment. So, 1 pixel away up through sqrt(2)
			 * away.
			 * 
			 * Whichever two points are within the right range will be "merged"
			 * so that the two outer points will describe the line segment.
			 */
			if (start.dst(lineSegment.start) <= Math.sqrt(2) + 1e-9) {
				start.set(lineSegment.end);
				return true;
			} else if (end.dst(lineSegment.start) <= Math.sqrt(2) + 1e-9) {
				end.set(lineSegment.end);
				return true;
			} else if (end.dst(lineSegment.end) <= Math.sqrt(2) + 1e-9) {
				end.set(lineSegment.start);
				return true;
			} else if (start.dst(lineSegment.end) <= Math.sqrt(2) + 1e-9) {
				start.set(lineSegment.start);
				return true;
			}

			return false;
		}

		/**
		 * Returns a pretty description of the LineSegment.
		 * 
		 * @return String
		 */
		@Override
		public String toString() {
			return "[" + start.x + "x" + start.y + "] -> [" + end.x + "x"
					+ end.y + "]";
		}
	}
}
